/*
  author Sylvain Bertrand <digital.ragnarok@gmail.com>
  Protected by GNU Affero GPL v3 with some exceptions.
  See README at root of alga tree.
*/
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <asm/unaligned.h>
#include <linux/mutex.h>
#include <linux/types.h>

#include <alga/amd/atombios/atb.h>
#include <alga/amd/atombios/dp_paths.h>

#include "tables/atb.h"
#include "tables/data.h"
#include "tables/obj_hdr.h"
#include "tables/gpio_pin_lut.h"
#include "tables/records.h"

#include "atb.h"

struct ctx
{
	struct atombios *atb;
	struct master_data_tbl *master_data_tbl;

	struct atb_dp_path *dp_paths;
	size_t dp_paths_n;

	struct obj_hdr *obj_hdr;
	struct path_tbl *path_tbl;
	struct obj_tbl *conn_tbl;
	struct obj_tbl *encoder_tbl;
};

static int ctx_init(struct atombios *atb, struct ctx *ctx)
{
	ctx->master_data_tbl = atb->adev.rom + get_unaligned_le16(
					&atb->hdr->master_data_tbl_offset);

	ctx->obj_hdr = atb->adev.rom + get_unaligned_le16(
					&ctx->master_data_tbl->list.obj_hdr);

	ctx->path_tbl = (void*)ctx->obj_hdr + get_unaligned_le16(
						&ctx->obj_hdr->path_tbl_offset);

	ctx->conn_tbl = (void*)ctx->obj_hdr + get_unaligned_le16(
						&ctx->obj_hdr->conn_tbl_offset);

	ctx->encoder_tbl = (void*)ctx->obj_hdr + get_unaligned_le16(
					&ctx->obj_hdr->encoder_tbl_offset);

	ctx->atb = atb;
	ctx->dp_paths = NULL;
	ctx->dp_paths_n = 0;
	return 0;
}

static unsigned grphs_n_compute(struct path *p)
{
	unsigned sz;
	sz = get_unaligned_le16(&p->sz);
	return (sz - offsetof(struct path, grph_ids)) / sizeof(__le16);
}

static bool is_valid_rec(struct rec_hdr *r)
{
	return r->sz > 0 && r->type > 0 && r->type < REC_TYPE_MAX;
}

static void uniphy_link_recs_parse(struct ctx *ctx, struct obj *u,
						struct encoder_caps **c)
{
	struct rec_hdr *r;

	r = (void*)ctx->obj_hdr + get_unaligned_le16(&u->rec_offset);

	while (is_valid_rec(r)) {
		if (r->type == REC_TYPE_ENCODER_CAPS) {
			*c = (struct encoder_caps*)r;
			break;
		}
		r = (void*)r + r->sz;
	}
}

static struct obj *objs_parse(struct obj_tbl *tbl, u16 id)
{
	unsigned i;
	
	for (i = 0; i < tbl->n; ++i) {
		struct obj *o;
		u16 o_id;

		o = (&tbl->objs) + i;
		o_id = get_unaligned_le16(&o->id);
		
		if (o_id == id)
			return o;
	}
	return NULL;
}

static void uniphy_link_hbr2_get(struct ctx *ctx, u16 id, bool *hbr2)
{
	struct obj *uniphy_link;

	uniphy_link = objs_parse(ctx->encoder_tbl, id);

	if (uniphy_link) {
		struct encoder_caps *caps;

		caps = NULL;
		uniphy_link_recs_parse(ctx, uniphy_link, &caps);

		if (caps)
			*hbr2 = caps->flgs & ENCODER_CAPS_DP_HBR2;
	}
}

/*
 * Get the dp uniphy transmitter link.
 * Encoder term is used for transmitter link here
 */
static int path_grphs_parse(struct ctx *ctx, unsigned idx, struct path *p)
{
	size_t grphs_n;
	u16 id;
	u8 sub_id;
	u8 type;

	grphs_n = grphs_n_compute(p);	
	if (grphs_n != 1) {
		dev_err(ctx->atb->adev.dev, "atombios: error, only one encoder"
			"graphic object is supported (grphs_n=%zu)\n", grphs_n);
		return -ATB_ERR;
	}

	id = get_unaligned_le16(&p->grph_ids);
	sub_id = id_decode_sub_id(id);
	type = id_decode_type(id);

	if (type != GRPH_TYPE_ENCODER
			&& sub_id != ENCODER_SUB_ID_INTERNAL_UNIPHY0
			&& sub_id != ENCODER_SUB_ID_INTERNAL_UNIPHY1
			&& sub_id != ENCODER_SUB_ID_INTERNAL_UNIPHY2) {
		dev_err(ctx->atb->adev.dev, "atombios: error, only uniphy "
			"link encoder graphic object is supported "
							"(id=0x%04x)\n", id);
		return -ATB_ERR;
	}
	ctx->dp_paths[idx].i = trans(id) * 2 + link_type(id);
	ctx->dp_paths[idx].uniphy_link_hbr2 = false;
	uniphy_link_hbr2_get(ctx, id, &ctx->dp_paths[idx].uniphy_link_hbr2);
	return 0;
}

static void hpd_parse(struct ctx *ctx, struct rec_hdr *r,
						struct atb_dp_path *atb_p)
{
	struct gpio_pin_lut *t;
	size_t n;
	struct hpd_int *h;
	unsigned i;

	t = ctx->atb->adev.rom + get_unaligned_le16(
				&ctx->master_data_tbl->list.gpio_pin_lut);

	n = (get_unaligned_le16(&t->hdr.sz) - sizeof(t->hdr)) / sizeof(t->pin);

	atb_p->hpd = 0xff;	/* no hpd */
	h = (struct hpd_int *)r;
	for (i = 0; i < n; ++i) {
		struct gpio_pin_assignment *pin;

		pin = &t->pin + i;

		if (h->gpio_id == pin->id) {
			switch (pin->bit_shift) {
			case 0:
				atb_p->hpd = 0;
				break;
			case 8:
				atb_p->hpd = 1;
				break;
			case 6:
				atb_p->hpd = 2;
				break;
			case 24:
				atb_p->hpd = 3;
				break;
			case 26:
				atb_p->hpd = 4;
				break;
			case 28:
				atb_p->hpd = 5;
				break;
			default:
				dev_warn(ctx->atb->adev.dev, "atombios: no " 
					"valid pin found for hpd gpio rec "
							"id=%u\n", h->gpio_id);
				break;
			}
		}
	}
}

static int conn_parse(struct ctx *ctx, struct obj *c, struct atb_dp_path *atb_p)
{
	struct rec_hdr *r;
	bool aux_found;

	aux_found = false;

	r =(void*)ctx->obj_hdr + get_unaligned_le16(&c->rec_offset);

	while (is_valid_rec(r)) {
		struct i2c_info *i;
		switch (r->type) {
		case REC_TYPE_I2C:
			aux_found = true;
			i = (struct i2c_info*)r;
			atb_p->aux_i2c_id = i->id;
			break;
		case REC_TYPE_HPD_INT:
			hpd_parse(ctx, r, atb_p);
			break;
		}
		r = (void*)r + r->sz;
	}

	if (!aux_found)
		return -ATB_ERR;
	return 0;
}

static int path_conn_parse(struct ctx *ctx, struct path *p,
						struct atb_dp_path *atb_p)
{
	u16 id;
	struct obj *c;

	id = get_unaligned_le16(&p->conn_id);

	c = objs_parse(ctx->conn_tbl, id);
	if (!c) {
		dev_err(ctx->atb->adev.dev, "atombios: unable to find "
					"connector object (id=0x%04x)\n", id);
		return -ATB_ERR;
	}
	return conn_parse(ctx, c, atb_p);
}

static int dfp_get(struct ctx *ctx, struct path *p, unsigned *dfp)
{
	unsigned i;
	*dfp = DFPx_MAX; /* invalid value */
	for (i = 0; i < DFPx_MAX; ++i) {
		if (get_unaligned_le16(&p->disp_dev) & vals_dfp_support[i]) {
			if (*dfp == DFPx_MAX) {
				*dfp = i;
			} else {
				dev_err(ctx->atb->adev.dev, "atombios: a "
						"displayport path has 2 DFP "
								"devices\n");
				return -1;
			}
		}
	}
	if (*dfp == DFPx_MAX) {
		dev_err(ctx->atb->adev.dev, "atombios: a displayport path has "
							"no DFP devices\n");
		return -ATB_ERR;
	}
	return 0;
}

static int path_parse(struct ctx *ctx, unsigned idx, struct path *p)
{
	u8 sub_id;
	int r;

	sub_id = id_decode_sub_id(get_unaligned_le16(&p->conn_id));
	
	ctx->dp_paths[idx].edp = (sub_id == CONN_SUB_ID_EDP);
	r = dfp_get(ctx, p, &ctx->dp_paths[idx].dfp);
	if (r != 0)
		return r;

	r = path_grphs_parse(ctx, idx, p);
	if (r != 0)
		return r;
	return path_conn_parse(ctx, p, ctx->dp_paths + idx);
}

static int dp_paths_n_func(struct ctx *ctx, unsigned idx, struct path *p)
{
	(void)idx;
	(void)p;
	++(ctx->dp_paths_n);
	return 0;
}

static bool is_dp(u16 id)
{
	u8 sub_id;
	u8 type;

	sub_id = id_decode_sub_id(id);
	type = id_decode_type(id);	

	return type == GRPH_TYPE_CONN && (sub_id == CONN_SUB_ID_DP
						|| sub_id == CONN_SUB_ID_EDP);
}

static int paths_parse_do(struct ctx *ctx, int (*dp_f)(struct ctx *ctx,
					unsigned dp_path_idx, struct path *p))
{
	u16 disp_dev_support;
	unsigned i;
	unsigned p_offset;
	unsigned dp_path_idx;
	int r;

	disp_dev_support = get_unaligned_le16(&ctx->obj_hdr->disp_dev_support);

	r = 0;
	p_offset = 0;
	dp_path_idx = 0;
	for (i = 0; i < ctx->path_tbl->n; ++i) {
		struct path *p;

		p = (struct path*)((void*)(&ctx->path_tbl->paths) + p_offset);
		p_offset += get_unaligned_le16(&p->sz);

		if ((disp_dev_support & get_unaligned_le16(&p->disp_dev))
				&& is_dp(get_unaligned_le16(&p->conn_id))){
			r = dp_f(ctx, dp_path_idx, p);
			++dp_path_idx;
			if (r != 0)
				break;
		}
	}
	return r;
}

/*
 * a display device in atombios context is a dfp or tv or another dfp... from
 * an atombios perspective, a graphic board support some of those display
 * devices, each one lighting a bit in obj_hdr->disp_dev_support
 */
static int paths_parse(struct ctx *ctx)
{
	int r;

	paths_parse_do(ctx, dp_paths_n_func); /* count valid dp paths */

	r = 0;
	if (ctx->dp_paths_n) {
		ctx->dp_paths = kzalloc(sizeof(*ctx->dp_paths)
						* ctx->dp_paths_n, GFP_KERNEL);
		if (!ctx->dp_paths)
			return -ATB_ERR;

		r = paths_parse_do(ctx, path_parse);
		if (r != 0)
			kfree(ctx->dp_paths);
	}
	return r;
}

int atb_dp_paths(struct atombios *atb, struct atb_dp_path **dp_paths,
							size_t *dp_paths_n)
{
	struct ctx ctx;
	int r;

	mutex_lock(&atb->mutex);
	ctx_init(atb, &ctx);

	r = paths_parse(&ctx);
	if (r != 0)
		goto err_free_mutex;

	*dp_paths = ctx.dp_paths;
	*dp_paths_n = ctx.dp_paths_n;

err_free_mutex:
	mutex_unlock(&atb->mutex);
	return r;
}
EXPORT_SYMBOL_GPL(atb_dp_paths);
